package santiago.domain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import santiago.exception.AlreadyInGameException;
import santiago.exception.ExistingItemException;
import santiago.exception.GameAboardedException;
import santiago.exception.InvalidAmountException;
import santiago.exception.InvalidPlayerException;
import santiago.exception.MaximalPlayerCountReachedException;
import santiago.exception.NotEnoughCashException;
import santiago.exception.NotEnoughPlayerException;

public class Game implements Serializable {

    private GameParameters Parameters;
    private Gameboard Gameboard;
    private Map<String, Player> Players;
    private Player[] PlayerSequence;
    private List<ColorType> AvailableColors;
    private byte CurrentRound;
    private byte CurrentStage;
    private Plantation[] CurrentPlantationLot;
    private int PlantationPlaced;
    private Auction Auction;
    private BribeSession BribeSession;
    private int CanalOverseer;
    private int CurrentPlayer;

    /**
     *
     * Instanciate a new Game to the given Name. By default, player's cash is
     * hidden
     *
     * @param GameName ID of the Game
     * @param PlayerNbr
     * @param DisplayCash
     * @param Level
     */
    public Game(String GameName, int PlayerNbr, DifficultyLevel Level, boolean DisplayCash, boolean PalmTree) {
        Parameters = new GameParameters(GameName, (byte) PlayerNbr, Level, DisplayCash, PalmTree);
        Players = new HashMap<>();
        AvailableColors = new ArrayList<>();
        AvailableColors.add(ColorType.BEIGE);
        AvailableColors.add(ColorType.BLACK);
        AvailableColors.add(ColorType.PURPLE);
        AvailableColors.add(ColorType.GRAY);
        AvailableColors.add(ColorType.WHITE);
    }

    /**
     * Add a player if he isn't already in the Game. Randomly initializes his
     * color
     *
     * @param Player
     * @throws ExistingItemException
     * @throws MaximalPlayerCountReachedException
     * @throws AlreadyInGameException
     */
    public void addPlayer(Player Player) throws ExistingItemException, MaximalPlayerCountReachedException, AlreadyInGameException {
        if (!isFull()) {
            if (Players.containsKey(Player.getName())) {
                throw new ExistingItemException();
            }
            Player.joinGame(Parameters.getName(), AvailableColors.remove((int) (Math.random() * AvailableColors.size())));
            Players.put(Player.getName(), Player);
        } else {
            throw new MaximalPlayerCountReachedException();
        }
    }

    /**
     * Removes a player of the game and make his color available back.
     *
     * @param Login
     * @throws GameAboardedException Thrown if the game has already begun,
     * allowing the application to deal with that specific case
     */
    public void removePlayer(String Login) throws GameAboardedException {
        Player Player = Players.remove(Login);
        AvailableColors.add(Player.getColor());
        Player.leaveGame();
        if (isStarted()) {
            throw new GameAboardedException("A player left the game");
        }
    }

    /**
     * Initialize the game and gameboard. Also randomly chooses the first player
     *
     * @throws NotEnoughPlayerException
     */
    public void start() throws NotEnoughPlayerException {
        synchronized (this) {
            if (!canStart()) {
                throw new NotEnoughPlayerException();
            }
            Collection<Player> C = Players.values();
            for (Player Player : C) {
                Player.startGame();
                try {
                    Player.receiveCash(10);
                } catch (InvalidAmountException ex) {
                }
            }
            CurrentRound = 1;
            CurrentStage = 0;
            Gameboard = new Gameboard(Parameters);
            PlayerSequence = new Player[Parameters.getPlayerCount()];
            Players.values().toArray(PlayerSequence);
            CurrentPlayer = CanalOverseer = (int) (Math.random() * Parameters.getPlayerCount());
            auction();
        }
    }

    /**
     * Aboard a game, kicks-off all players
     */
    public void aboard() {
        for (Map.Entry<String, Player> entry : Players.entrySet()) {
            entry.getValue().leaveGame();
        }
        Players.clear();
        Gameboard = null;
        PlayerSequence = null;
    }

    public void auction() {
        CurrentStage = 1;
        CurrentPlantationLot = Gameboard.drawPlantationLot();
        Auction = new Auction(Parameters.getPlayerCount());
    }

    public Plantation[] getCurrentPlantationLot() {
        return CurrentPlantationLot;
    }

    public void bid(String Login, int Amount) throws InvalidAmountException, NotEnoughCashException {
        Auction.addBid(Players.get(Login), Amount);
        nextPlayer();
        if (isStageOver()) {
            changeCanalOverseer();
        }
    }

    public int getPlayerBid(String Login) {
        return Auction.getPlayerBid(Players.get(Login));
    }

    private void changeCanalOverseer() {
        CurrentStage = 2;
        PlayerSequence = Auction.getPlayerSequence();
        CurrentPlayer = CanalOverseer = PlantationPlaced = 0;
    }

    public Player getCanalOverseer() {
        return Auction.getCanalOverseer();
    }

    public void placePlantation(String Login, int NodeIndex, int PlantationIndex, CardinalPoint Location) {
        Player Player = Players.get(Login);
        Plantation Plantation = CurrentPlantationLot[PlantationIndex];
        Field Field = Gameboard.getNodes()[NodeIndex].getLot(CardinalPoint.SOUTH_EAST).getField(Location);
        int YieldMarkersCount;
        if (Parameters.getPlayerCount() == 3 && CurrentPlayer == 0 && PlantationPlaced == CurrentPlantationLot.length - 1) {
            YieldMarkersCount = 0;
        } else {
            int BidAmount = Auction.getPlayerBid(Player);
            YieldMarkersCount = BidAmount == 0 ? Plantation.getCapacity() - 1 : Plantation.getCapacity();
        }
        for (int i = 0; i < YieldMarkersCount; i++) {
            Plantation.addYieldMarker(new YieldMarker(Player.getColor()));
        }
        Field.setPlantation(Plantation);
        PlantationPlaced++;
        nextPlayer();
        if (isStageOver()) {
            bribeCanalOverseer();
        }
    }

    private void bribeCanalOverseer() {
        CurrentStage = 3;
        BribeSession = new BribeSession(Parameters.getPlayerCount());
    }

    public void bribe(String Login, String LoginOfSupportedPlayer, int Amount, int NodeIndex, CardinalPoint Location) throws InvalidAmountException {
        Player Player = Players.get(Login);
        Player SupportedPlayer = Players.get(LoginOfSupportedPlayer);
        Canal Canal = Gameboard.getNodes()[NodeIndex].getCanal(Location);
        BribeSession.addBribe(Player, SupportedPlayer, Amount, Canal);
        nextPlayer();
        if (isStageOver()) {
            canalOverseerChoice();
        }
    }

    private void canalOverseerChoice() {
        CurrentStage = 4;
    }

    public void acceptBribe(String Login) throws InvalidPlayerException, InvalidAmountException, NotEnoughCashException {
        Player Player = Players.get(Login);
        Bribe Bribe = BribeSession.getBribe(Player);
        Player[] Winners = Bribe.getPlayers();
        for (int i = 0; i < Winners.length; i++) {
            Winners[i].pay(Bribe.getAmount(Winners[i]), PlayerSequence[CanalOverseer]);
        }
        conductExtraIrrigation();
    }

    public void outBidBribe(String Login) throws InvalidAmountException, NotEnoughCashException {
        Player Player = Players.get(Login);
        Bribe Bribe = BribeSession.getBribe(Player);
        PlayerSequence[CanalOverseer].pay(Bribe.getTotalAmount() + 1);
        conductExtraIrrigation();
    }

    private void conductExtraIrrigation() {
        CurrentStage = 5;
    }

    private void nextPlayer() {
        CurrentPlayer = (CurrentPlayer + 1) % Parameters.getPlayerCount();
    }

    public boolean isStageOver() {
        switch (CurrentStage) {
            case 1:
                return Auction.hasFinished();
            case 2:
                return PlantationPlaced == CurrentPlantationLot.length;
            case 3:
                return BribeSession.hasFinished();
            default:
                return CurrentPlayer == 0;
        }
    }

    @Override
    public boolean equals(Object o) {
        if (o != null && o instanceof Game) {
            Game Game = (Game) o;
            return Game.getName().toLowerCase().equals(this.getName().toLowerCase());
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 13 * hash + Objects.hashCode(Parameters.getName());
        return hash;
    }

    public String getName() {
        return Parameters.getName();
    }

    public GameParameters getParameters() {
        return Parameters;
    }

    public Plantation getDeletedPlantation() {
        return Parameters.getDeletedPlantation();
    }

    public int getCurrentPlayerCount() {
        return Players.size();
    }

    public Player getCurrentPlayer() {
        return PlayerSequence[CurrentPlayer];
    }

    public int getCurrentPlayerIndex() {
        return CurrentPlayer;
    }

    public Collection<Player> getPlayers() {
        return Players.values();
    }

    public Player[] getPlayerSequence() {
        return PlayerSequence;
    }

    public boolean hasPlayer() {
        return Players.size() > 0;
    }

    public boolean isFull() {
        return Players.size() >= Parameters.getPlayerCount();
    }

    /**
     * @return <code>true</code> if there is enough player to start a game
     */
    public boolean canStart() {
        return Players.size() == Parameters.getPlayerCount();
    }

    public boolean isStarted() {
        return Gameboard != null;
    }

    public byte getCurrentRound() {
        return CurrentRound;
    }

    public byte getTotalRounds() {
        return Parameters.getRoundNbr();
    }

    public byte getCurrentStage() {
        return CurrentStage;
    }

    public Node[] getNodes() {
        return Gameboard.getNodes();
    }

    public int getSpring() {
        return Gameboard.getSpring();
    }
}
